# coding=utf-8
import sys
import traceback

import pytest
import operator
import prettytable as pt
from pactverify.matchers import Matcher, Like, EachLike, Term, Enum, PactVerify
from common.logger import Logger

def log_print(fun):
    """
    校验后日志打印
    :param Expected:
    :param Actual:
    :param bool:
    :return:
    """

    def wrapped(*args):
        global tb, Field_name
        # 判断是否由string_assert类里面的方法使用，以便确定参数位置，类使用此装饰器时，第一个参数是类本身
        if isinstance(args[0], string_assert):
            # string_assert 不会有第三个参数
            Expected = args[1]
            Actual = args[2]
        else:
            Expected = args[0]
            Actual = args[1]
            try:
                Field_name = args[2]    # list_each_equal传入的第三个参数，则有列名
            except IndexError:
                Field_name= None
        # 获取到断言操作和断言结果，比如('大于',True)，然后进行日志输出
        result = fun(*args)
        # Expected为int时，len(Expected)会报错。且长度不相等就直接打印表格样式了，否则prettytable无法打印出来
        if not isinstance(Expected,int) and len(Expected) != len(Actual):
            Logger.error(f"校验失败，元素个数不相等。预期【{len(Expected)}】，实际【{len(Actual)}】")
            Logger.error(f"校验失败，校验预期值【{Expected}】 {result[0]} 实际值：【{Actual}】失败！")
            pytest.assume(False, f"校验失败:元素个数不相等。预期【{len(Expected)}】，实际【{len(Actual)}】。\n校验预期值:【{Expected}】\n实际值：【{Actual}】")
        else:
            # 组装list表格
            if isinstance(Expected,list):
                tb = pt.PrettyTable()
                Expected.insert(0,'预期值')
                Actual.insert(0,'实际值')
                if Field_name:  # 如果传入了列名，将列名插入到表头。
                    Fields=[]
                    Fields.extend(Field_name)   # 需要额外定义Fields列表，初始化后，传入Field_name，否则每次都会重复插入.insert(0, '---')
                    Fields.insert(0, '---')
                    tb = pt.PrettyTable(Fields) # 定义表格列名
                tb.add_row(Expected)
                tb.add_row(Actual)
            elif isinstance(Expected,dict):
                # 定义第行为列名，第二行为预期值，第三行为实际值
                head,Expected_row,Actual_row=[],[],[]
                # 必须分3步，先转dict转list，再批量扩展head列表，然后插入第0列
                head.extend(list(Expected))
                head.insert(0, '---')
                Expected_row.extend(list(Expected.values()))
                Expected_row.insert(0, '预期值')
                Actual_row.extend(list(Actual.values()))
                Actual_row.insert(0, '实际值')
                tb = pt.PrettyTable(head)
                tb.add_row(Expected_row)
                tb.add_row(Actual_row)
            if result[1] is False:
                Logger.info(f"校验失败，校验预期值【{Expected}】 {result[0]} 实际值：【{Actual}】失败！")
                diff_detail = JsonCompare(Actual, Expected) # 校验错误后，进行具体错误对比
                # 打印列表格式的表格
                if isinstance(Expected, list):
                    Logger.info(f"校验失败:\n{tb}")
                    # 获取具体差异处，并输出在异常处
                    diff_result = diff_detail.compare_result
                    pytest.assume(False, f"校验失败:\n{tb}\n具体差异：\n{diff_result}")
                    # pytest.assume(False, f"校验失败:\n{tb}")
                # 打印字典格式的表格
                elif isinstance(Expected, dict):
                    Logger.info(f"校验失败:\n{tb}")
                    # 获取具体差异处，并输出在异常处
                    diff_result = diff_detail.compare_result
                    pytest.assume(False, f"校验失败:\n{tb}\n具体差异：\n{diff_result}")
                    # pytest.assume(False,f"校验失败:\n{tb}")
            else:
                Logger.info(f"校验成功，校验预期值【{Expected}】 {result[0]} 实际值：【{Actual}】成功！")
                # 打印列表格式的表格
                if isinstance(Expected, list):
                    Logger.info(f"校验成功:\n{tb}")
                # 打印字典格式的表格
                elif isinstance(Expected, dict):
                    Logger.info(f"校验成功:\n{tb}")

    return wrapped


@log_print
def list_assert_equal(Expected_list1, actual_list2, Field_name=None, order: bool = True):
    """
    断言列表是否相等，order是否排序后对比，默认排序后对比
    :param Expected_list1: 预期列表
    :param actual_list2: 实际列表
    :param Field_name: 列表每个元素代表的字段名，用于对比后打印显示实际元素代表的字段名称。
    :param order: 是否列表排序后断言，默认排序后断言，也就是忽略列表元素位置
    :return:
    """
    # 列表比较：https://www.cnblogs.com/lsdb/p/10275657.html
    # print(Field_name)
    if order == True:
        if sorted(Expected_list1) == sorted(actual_list2):
            return '等于', True
        else:
            return '等于', False
        # return '等于', pytest.assume(sorted(Expected_list1) == sorted(actual_list2))
    else:
        if Expected_list1 == actual_list2:
            return '等于', True
        else:
            return '等于', False
        # return '等于', pytest.assume(Expected_list1 == actual_list2)

def list_each_equal(expect, actual, show_fields=None):
    """
    二维列表断言，expect预期二维列表，actual实际二维列表，show_fields是否展示二维列表字段名
    :param expect: 预期值
    :param actual: 实际值
    :param show_fields: 此参数不传，或者参数传list时，显示字段名称进行对比
    :return:
    """
    if isinstance(show_fields,list) or show_fields==None:
        # 先校验列表长度
        if len(expect)==len(actual):
            pytest.assume(True)
        else:
            tips = f'校验失败：长度不符：实际长度【{len(actual)}】，预期长度：【{len(expect)}】\n实际值：【{actual}】，与预期值【{expect}】不符'
            pytest.assume(False,tips)
            Logger.error(tips)
        # assert len(expect)==len(actual),Logger.error(f'校验失败：长度不符：实际长度【{len(actual)}】，预期长度：【{len(expect)}】\n实际值：【{actual}】，与预期值【{expect}】不符')
        for index, value in enumerate(actual):
            # 如果有字段名，那么二维中index=0时是字段名，就不纳入断言了
            if index != 0:
                try:
                    list_assert_equal(expect[index], value, actual[0]) # 处理掉index异常，方便后续的预期结果
                except IndexError:
                    pass
    else:
        # 先校验列表长度
        if len(expect)==len(actual):
            pytest.assume(True)
        else:
            tips = f'校验失败：长度不符：实际长度【{len(actual)}】，预期长度：【{len(expect)}】\n实际值：【{actual}】，与预期值【{expect}】不符'
            pytest.assume(False,tips)
            Logger.error(tips)
        # assert len(expect)==len(actual),Logger.error(f'校验失败：长度不符：实际长度【{len(actual)}】，预期长度：【{len(expect)}】\n实际值：【{actual}】，与预期值【{expect}】不符')
        # 如果没有字段名，就直接断言每个列表
        for index, value in enumerate(actual):
            list_assert_equal(expect[index], value)

def list_assert_contains(expect, value):
    """
    value内的元素存在于expect中
    :param expect:
    :param value:
    :return:
    """
    if value in expect:
        pytest.assume(True)
        return True
    else:
        tips = f'校验失败：实际值：【{value}】不在预期值【{expect}】之中'
        pytest.assume(False,tips)
        Logger.error(tips)
        return False
    # assert (value in expect),Logger.error(f'校验失败：实际值：【{value}】不在预期值【{expect}】之中')

def two_array_list_contains(expect, actual):
    """
    用于断言数据库查询时无法排序的二维列表。二维列表actual[*]的所有列表元素存在于expect中
    :param expect:
    :param actual:
    :return:
    """
    # 先校验列表长度
    if len(expect) == len(actual):
        pytest.assume(True)
    else:
        tips = f'校验失败：长度不符：实际长度【{len(actual)}】，预期长度：【{len(expect)}】\n实际值：【{actual}】，与预期值【{expect}】不符'
        pytest.assume(False,tips)
        Logger.error(tips)
    # assert len(expect) == len(actual), Logger.error(f'校验失败：长度不符：实际长度【{len(actual)}】，预期长度：【{len(expect)}】\n实际值：【{actual}】，与预期值【{expect}】不符')
    # 再校验所有查询结果，都存在于预期结果中，用包含时，断言一个元素，删掉一个元素
    result = []
    for value in actual:
        result.append(list_assert_contains(expect,value))
    if False not in result:
        Logger.info(f'校验成功：\n实际值【{actual}】所有元素均存在于【{expect}】中')
    else:
        Logger.info(f'校验失败：\n实际值【{actual}】有元素不存在于【{expect}】中')

@log_print
def dict_assert(Expected_dict, Actual_dict):
    """
    第一个入参是预期字典，第二个入参是实际字典
    """
    # operator.eq对比字典，自动排序后对比
    # 函数                    含义
    # operator.lt(a, b)    a < b
    # operator.le(a, b)    a <= b
    # operator.eq(a, b)    a == b
    # operator.ne(a, b)    a != b
    # operator.gt(a, b)    a > b
    # operator.ge(a, b)    a >= b
    # :param Expected_dict:
    # :param Actual_dict:
    # :return:
    if operator.eq(Expected_dict, Actual_dict):
        return '等于', True
    else:
        return '等于', False
    # return '等于', pytest.assume(operator.eq(Expected_dict, Actual_dict))

class Pact:
    """
    契约格式类，契约校验文档：https://pypi.org/project/pactverify/
    """

    def pact_verify(self,contract_format, resp_json):
        """
        契约校验执行
        :param contract_format: 契约格式
        :param resp_json: 响应体json
        :return:
        """
        mPactVerify = PactVerify(contract_format)
        actual_rsp_json = resp_json
        try:
            mPactVerify.verify(actual_rsp_json)
            assert mPactVerify.verify_result == True,resp_json
            Logger.info(f'契约校验成功：{mPactVerify.verify_result}')
        except Exception:
            Logger.error(f'契约校验失败：{mPactVerify.verify_info}')
            assert mPactVerify.verify_result == True,f'契约校验失败！失败信息：{mPactVerify.verify_info}\n接口实际返回：\n{actual_rsp_json}'

    def initiate_pact_format(self):
        """
        发起流程响应的契约格式
        :return:
        """
        contract_format = Matcher(
            {
                "code": 1,
                "data": Matcher({
                    "proc_id": Term(r"(\w+)-(\w+)-(\w+)-(\w+)-(\w+)", example="f97b340b-085f-587d-5874-39fdf94cd978"),
                    "node_id": Term(r"(\w+)-(\w+)-(\w+)-(\w+)-(\w+)", example="f97b340b-085f-587d-5874-39fdf94cd978"),
                    "package_id": Term(r"\w{32}|(\w+)-(\w+)-(\w+)-(\w+)-(\w+)", example="60c72ad005d3e1452202121091406326"),
                    "parent_id": Term(r"\w{32}", example="60c72ad005d3e1452202121091406326", nullable=True)
                }),
                "message": "",
                "detailMessage": "",
                "success": 1
            })
        return contract_format

    def save_draft_pact_format(self):
        """
        保存草稿响应的契约格式
        :return:
        """
        contract_format = Matcher(
            {
                "code": 1,
                "data": Matcher({
                    "proc_id": Term(r"(\w+)-(\w+)-(\w+)-(\w+)-(\w+)", example="f97b340b-085f-587d-5874-39fdf94cd978"),
                }),
                "message": "",
                "detailMessage": "",
                "success": 1
            })
        return contract_format

    def approve_pact_format(self):
        """
        审批流程响应的契约格式
        :return:
        """
        contract_format = Matcher({
            "code": 1,
            "data": Matcher({
                "isOk": True,
                "canPass": 1,
                "branch": [],
                "actuals": [],
                "transitions": [],
                # 多人审批时，步骤未办结，状态是2，办结是3，所以使用枚举
                "stepStatus": Enum(["2","3"]),
                "stepId": Term(r"(\w+)-(\w+)-(\w+)-(\w+)-(\w+)", example="f97b340b-085f-587d-5874-39fdf94cd978"),
                "isConfirmPath": False,
                "table_val_md5":Term(r"\w{32}", example="a9b626f3a0aa8bf29b138048c34ce5e5"),
                "passMessage": "",
                "validateResult": []
            }),
            "message": "",
            "detailMessage": "",
            "success": 1
        })
        return contract_format

    def approval_withdraw_pact_format(self):
        """
        审批撤回响应的契约格式
        :return:
        """
        contract_format = Matcher({
            "code": 1,
            "data": Matcher({
                "ProcessInstanceStatus": "0",
                "ProcessInstanceId": Term(r"(\w+)-(\w+)-(\w+)-(\w+)-(\w+)", example="f97b340b-085f-587d-5874-39fdf94cd978"),
                "TaskId": Term(r"(\w+)-(\w+)-(\w+)-(\w+)-(\w+)", example="f97b340b-085f-587d-5874-39fdf94cd978"),
                "PackageId": Term(r"\w{32}", example="60c72ad005d3e1452202121091406326"),
                "WithdrawNextTasks": EachLike("73167915-a8bd-ab0d-2562-3a11e2838592",minimum=1)
            }),
            "message": "",
            "detailMessage": "",
            "success": 1
        })
        return contract_format

    def roll_back_pact_format(self):
        """
        打回响应的契约格式
        :return:
        """
        contract_format = Matcher({"code": 1, "data": True, "message": "", "detailMessage": "", "success": 1})
        return contract_format

    def transfer_pact_format(self):
        """
        交办响应的契约格式
        :return:
        """
        contract_format = Matcher({"code": 1, "data": None, "message": "", "detailMessage": "", "success": 1})
        return contract_format

    def consult_pact_format(self):
        """
        协商响应的契约格式
        :return:
        """
        contract_format = Matcher({"code": 1, "data": None, "message": "", "detailMessage": "", "success": 1})
        return contract_format

    def cancel_pact_format(self):
        """
        作废流程响应的契约格式
        :return:
        """
        contract_format = Matcher({"code":1,"data":True,"message":"","detailMessage":"","success":1})
        return contract_format

    def termination_pact_format(self):
        """
        终止流程响应的契约格式
        :return:
        """
        contract_format = Matcher({"code":1,"data":None,"message":"","detailMessage":"","success":1})
        return contract_format

    def reminder_pact_format(self):
        """
        催办响应的契约格式
        :return:
        """
        contract_format = Matcher({"code":1,"data":None,"message":"","detailMessage":"","success":1})
        return contract_format

    def cirlulate_pact_format(self):
        """
        传阅响应的契约格式
        :return:
        """
        contract_format = Matcher({"code":1,"data":None,"message":"","detailMessage":"","success":1})
        return contract_format

    def addsign_pact_format(self):
        """
        加签响应的契约格式
        :return:
        """
        contract_format = Matcher({"code":1,"data":None,"message":"","detailMessage":"","success":1})
        return contract_format

class string_assert:
    """
    数字或者字符串断言
    """

    @log_print
    def eq(self, string1, string2):
        """
        string1等于string2
        :param string1:
        :param string2:
        :return:
        """
        return '等于', pytest.assume(str(string1) == str(string2),f'校验失败：\n实际值【{string1}】不等于【{string2}】')

    @log_print
    def lt(self, string1, string2):
        """
        string1小于string2
        :param string1:
        :param string2:
        :return:
        """
        return '小于', pytest.assume(str(string1) < str(string2))

    @log_print
    def le(self, string1, string2):
        """
        string1小于等于string2
        :param string1:
        :param string2:
        :return:
        """
        return '小于等于', pytest.assume(str(string1) <= str(string2))

    @log_print
    def ne(self, string1, string2):
        """
        string1不等于string2
        :param string1:
        :param string2:
        :return:
        """
        return '不等于', pytest.assume(str(string1) != str(string2))

    @log_print
    def gt(self, string1, string2):
        """
        string1大于string2
        :param string1:
        :param string2:
        :return:
        """
        return '大于', pytest.assume(str(string1) > str(string2))

    @log_print
    def ge(self, string1, string2):
        """
        string1大于等于string2
        :param string1:
        :param string2:
        :return:
        """
        return '大于等于', pytest.assume(str(string1) >= str(string2))

class JsonCompare:

    def __init__(self, first_data, second_data, debug_model=False):
        """
        用于两个json串比对，输出两串json字段差异

        :param second_data: 用于比对的新json，实际json
        :param first_data: 基准json,预期json
        :param debug_model: 为True时将在控制台输出比对结果，默认不开启
        """

        self.compare_result = []  # 存放字段差异
        self.compare_error = []  # 存放字段比对异常
        self.compare(first_data, second_data)

        if debug_model:
            for i in self.compare_result:
                print(i)
            for i in self.compare_error:
                print(i)

    def compare(self, first_data, second_data, path=''):
        try:
            if not isinstance(second_data, (list, tuple, dict)):
                # 比对非list\tuple\dict类型的数据
                if not second_data == first_data:
                    msg = {
                        '【字段位置】': path,
                        '【错误类型】': '字段值不匹配',
                        '【实际值】': second_data,
                        '【预期值】': first_data
                    }
                    self.compare_result.append(msg)

            elif isinstance(second_data, (list, tuple)):
                # 如果新数据是list/tuple,则判断老数据是否类型一致;不一致则追加到compare_error
                if type(second_data) != type(first_data):
                    msg = {
                        '【字段位置】': path,
                        '【错误类型】': "字段类型不匹配",
                        '【实际值】': f"type is {type(second_data)}",
                        '【预期值】': f"type is {type(first_data)}"
                    }
                    self.compare_result.append(msg)
                    return
                if len(second_data) != len(first_data):
                    msg = {
                        '【字段位置】': path,
                        '【错误类型】': "字段长度不匹配",
                        '【实际值】': f"Length of list is {len(second_data)}",
                        '【预期值】': f"Length of list is {len(first_data)}"
                    }
                    self.compare_result.append(msg)
                for index, value in enumerate(second_data):
                    try:
                        if index < len(first_data):
                            self.compare(
                                value, first_data[index], f'{path}:{index}')
                        else:
                            self.compare(value, {}, f'{path}:{index}')
                    except Exception as e:
                        self.compare_error.append(
                            f'Unknown error: {e.args}')
            else:
                # 比对值为dict类型数据
                if not isinstance(first_data, dict):
                    msg = {
                        '【字段位置】': path,
                        '【错误类型】': "字段类型不匹配",
                        '【实际值】': f"type is {type(second_data)}",
                        '【预期值】': f"type is {type(first_data)}"
                    }
                    self.compare_result.append(msg)
                    return
                new_keys = set(second_data.keys())
                old_keys = set(first_data.keys())
                diff_keys = old_keys - new_keys  # 用于检查老数据存在某个key，但新数据不存在。
                for key, value in second_data.items():
                    try:
                        if key in first_data.keys():
                            self.compare(
                                value, first_data[key], f"{path}:{key}")
                        else:
                            msg = {
                                '【字段位置】': f"{path}:{key}",
                                '【错误类型】': '字段缺失',
                                '【实际值】': value,
                                '【预期值】': f"Field of '{key}' is not exists"
                            }
                            self.compare_result.append(msg)
                    except Exception as e:
                        self.compare_error.append(
                            f'Unknown error:{e.args}')
                if diff_keys:
                    for key in diff_keys:
                        msg = {
                            '【字段位置】': f"{path}:{key}",
                            '【错误类型】': '字段缺失',
                            '【实际值】': f"Field of '{key}' is not exists",
                            '【预期值】': first_data[key]
                        }
                        self.compare_result.append(msg)

        except Exception as e:
            self.compare_error.append(
                f'Unknown error:{e.args}')

# 用于调试
# pact = Pact()
# pact_c = pact.approval_withdraw_pact_format()
# # json_str = {"code": 1, "data": {"ProcessInstanceStatus": "0", "ProcessInstanceId": "8adf7b40-0156-9208-799e-3a007c0dc4c0", "TaskId": "54bb3814-01e3-0f0f-f54f-3a007c0e298f", "PackageId": "61977793cb3c77025400086339374740", "WithdrawNextTasks": ["73167915-a8bd-ab0d-2562-3a11e2838592"]}, "message": "", "detailMessage": "", "success": 1}
# json_str = {"code": 1, "data": {"ProcessInstanceStatus": "0", "ProcessInstanceId": "8adf7b40-0156-9208-799e-3a007c0dc4c0", "TaskId": "54bb3814-01e3-0f0f-f54f-3a007c0e298f", "PackageId": "61977793cb3c77025400086339374740", "WithdrawNextTasks": []}, "message": "", "detailMessage": "", "success": 1}
# # json_str = {"code": 1, "data": {"ProcessInstanceStatus": "0", "ProcessInstanceId": "8adf7b40-0156-9208-799e-3a007c0dc4c0", "TaskId": "54bb3814-01e3-0f0f-f54f-3a007c0e298f", "PackageId": "61977793cb3c77025400086339374740", "message": "", "detailMessage": "", "success": 1}}
# pact.pact_verify(pact_c,json_str)
